<?php
/**
 * @file Model.php
 *
 * Contains an abstract class for ORM representation of tables.
 *
 * New BSD License
 * \n
 * Copyright (c) 2010 Rpd-Team
 *
 *  @todo A110103 make it possible to join with another Model e.g. by concatenation
 *
 * @namespace  rpd
 */

/**
 * Stores and delivers information.
 *
 * @since  0.1.0
 *
 * Models are one of the three important parts of MVC. The Controller acts
 * on the user input, get some information according to it from the Model and
 * show it to the client with a View.
 *
 * Often (Models) are ment to represent tables / sql statements as classes
 * (called ORM). Changes to this array will be executed in the database but
 * not in the model itself. Therefor you need to refresh sometimes.
 *
 *
 * @section Changelog
 *
 * <table>
 * 	<tr>
 * 		<th>Version</th>
 * 		<th>Description</th>
 *  </tr>
 *  <tr><td>Since 0.1.1</td><td>Other models joinable.</td></tr>
 * 	<tr><td>Since 0.1.1</td><td>Added Model::pipe (object) as alias of Model::__invoke (object)</td></tr>
 * </table>
 *
 *
 * @section Examples
 *
 * @subsection Generate_Model Generate Model
 *
 * Inherit from the model to generate representations of each table:
 *
 * @code
 * class Model_SomeName extends Model
 * {
 * 	private $registry = "db/std";   // use the "db/std"-connection from config
 * 	private $table = "tablename";   // specify the represented table(s)
 * }
 * @endcode
 *
 * The instance can be accessed like an object:
 *
 * @code
 * $mdl = new Model_SomeName();
 * foreach ($mdl as $row)
 * {
 * 	echo $row->field;            // Read
 * 	$row->field = "new value";   // Update
 * }
 * @endcode
 *
 * @subsection Conditions_Model Conditions
 *
 * Getting the whole table takes some time on your SQL-Server. Therefore
 * you can add a (where-)condition to the Model within the new class to
 * receive only a subset:
 * @code
 * class Model_MaleUser extends Model
 * {
 * 	private $where = "gender = 'male'";
 * }
 * @endcode
 *
 * Or during runtime with the new instance:
 * @code
 * $need = "male";
 * $all_male = new Model_Users("gender = '$need'");
 * @endcode
 *
 * \warning{
 * 	Using this may cause SQL-Injection,
 * 	Prevent it by escaping hazardous strings!
 *  Read how-to in Model::__construct.
 * }
 *
 * @subsection Model_JoinModel Join Model
 *
 * If you have according information (early) bind it to an existing Model as
 * a new one:
 * @code
 * $user = new Model_User("iduser = 123");  // get user with id "123"
 * $user->Rights = new Model_Rights();      // get according rights
 *
 * echo $user->Rights->write;  // will show the value for the specific user
 * @endcode
 * 
 * \nomagic{
 * 	Model::join() will do approximately the same:,
 * 	$user->join(new Model_Rights());
 * }
 * 
 * This magical way will do a LEFT JOIN like Model::join().
 *
 * \note{	Join only unused Models,
 * 			Only join at the beginning but not while iterating or
 * 			after using the Model.
 * }
 *
 * @see Controller
 * @see Model_Row
 * @see Registry
 * @see View
 */
class Model extends Db implements Iterator {



	/**
	 * Fields which are accessable
	 * @var unknown_type
	 */
	protected $fields   = "*";
	
	/**
	 * Used Connection (as stored in Registry)
	 * @var unknown_type
	 */
	protected $registry = "db/std";
	
	/**
	 * Table which will be represented
	 * @var string
	 */
	protected $table    = NULL;

	/**
	 * Condition for some subset of the table
	 * @var string
	 */
	protected $where    = "1";


	private $_joinedTables = array();
	
	private $_statement = NULL;
	
/**
	 * (non-PHPdoc)
	 * 
	 * 
	 * @throws IllegalArgument
	 * @see Pipeline::__invoke()
	 */
	function __invoke($in)
	{
		if (is_array($in))
		{
			/**
			 * @todo A110321 exceptions
			// don't support multi-arrays
			if (is_array(current($in)))
			{
				throw new
				IllegalArgument("Multi-Dimensional Array not supported");
			}
			*/
			
			// generate update if keys exists
			$keys = array_keys($in);
			$update = "";
			foreach ($keys as $key)
			{
				$update .= ", `$key` = VALUES($key)";
			}
			$update = substr($update, 1);
			
			// make query for insert and update if keys already exists
			$query = "INSERT INTO `" . $this->getTable() . "` (`"
			       . implode("`, ", keys) . "`) VALUES ("
			       . str_repeat("'%s'", count($in)) . ")"
			       . " ON DUPLICATE KEY UPDATE "
			       . $update;

			$this->query($query, $in);
		}
	}
	
	function __set($var, $value)
	{
		
		if (is_subclass_of($value, "Model"))
		{
			$this->join($value);
			$this->_joinedTables[strtolower($var)] = $model;
		}
	}
	
	public function current ()
	{
		return new Model_Row($this->_statement, $this);
	}

	/**
	 * Getter for the fields
	 *
	 * Returns the contained fields / columns of the Model.
	 * If it states "*" then every field of the tables (Model::getTable())
	 * will be used.
	 *
	 * @since 0.1.0
	 *
	 * @return string The fieldnames
	 *
	 * @section Changelog
	 *
	 * <table>
	 * 	<tr>
	 * 		<th>Version</th>
	 * 		<th>Description</th>
	 *  </tr>
	 *  <tr><td></td><td></td></tr>
	 * </table>
	 *
	 * @section Examples
	 *
	 * @subsection Model.getFields-Get Get fields
	 *
	 * <pre>/app/Controller/index.php</pre>
	 * @code
	 * <?php
	 *
	 * $mdl = new Model_Foo();	// generate instance
	 * echo $mdl->getFields();	// shows the fields
	 *
	 * ?>
	 * @endcode
	 *
	 * @see Model
	 */
	function getFields()
	{
		return $this->fields;
	}
	
	function getJoinedTables()
	{
		return $this->_joinedTables;
	}
	
	/**
	 * Getter for the tables
	 *
	 * Returns the used tables from the database which are represented.
	 *
	 * @since 0.1.0
	 *
	 * @return string The tablenames
	 *
	 * @section Changelog
	 *
	 * <table>
	 * 	<tr>
	 * 		<th>Version</th>
	 * 		<th>Description</th>
	 *  </tr>
	 *  <tr><td></td><td></td></tr>
	 * </table>
	 *
	 * @section Examples
	 *
	 * @subsection Model.getTable-Get Get tablename
	 *
	 * <pre>/app/Controller/index.php</pre>
	 * @code
	 * <?php
	 *
	 * $mdl = new Model_Foo();	// generate instance
	 * echo $mdl->getTable();	// shows the table
	 *
	 * ?>
	 * @endcode
	 *
	 * @see Model
	 */
	function getTable()
	{
		return $this->table;
	}

	/**
	 * Getter for the where clause
	 *
	 * Returns the used (where) conditions for the Model
	 *
	 * @since 0.1.0
	 *
	 * @return string The (where) condition
	 *
	 * @section Changelog
	 *
	 * <table>
	 * 	<tr>
	 * 		<th>Version</th>
	 * 		<th>Description</th>
	 *  </tr>
	 *  <tr><td></td><td></td></tr>
	 * </table>
	 *
	 * @section Examples
	 *
	 * @subsection Model.getWhere-Get Get conditions
	 *
	 * <pre>/app/Controller/index.php</pre>
	 * @code
	 * <?php
	 *
	 * $mdl = new Model_Foo();	// generate instance
	 * echo $mdl->getWhere();	// shows the used where clause
	 *
	 * ?>
	 * @endcode
	 *
	 * @see Model
	 */
	function getWhere()
	{
		return $this->where;
	}

	function isJoinedTable($tablename = "")
	{
		var_dump($this->_joinedTables);
		return isset($this->_joinedTables[strtolower($tablename)]);
	}
	
	/**
	 * Joins two Models
	 * 
	 * @param Model $model Another Model to join with
	 * 
	 * @todo A110228 RIGHT LEFT INNER OUTER NATURAL CROSS
	 */
	function join($model)
	{
		/**
		 * @todo make late binding possible
		 * @todo performance by checking if $name exists
		 */
		// join fields
		if ($this->fields != "*" && $model->getFields() != "*")
		{
			$this->fields .= ", " . $model->fields;
		}

		// table
		$where = $model->getWhere();
		if ($where == "1")
		{
			$table = RPD::ModelToTablename(get_class($this));
			$target = RPD::ModelToTablename(get_class($model));

			$where = "$table.id$table = $target.id$table";
		}

		$this->table .= " LEFT JOIN `" . $model->getTable() . "` ON ("
		              . $where . ")";
	}
	
	public function key ()
	{
		return $this->_statement->key();
	}

	/**
	 * @todo C1104 array access
	 */

	public function next ()
	{
		$this->_statement->next();
	}
	
	public function refresh()
	{
		$query = "SELECT " . $this->fields . " FROM " . $this->table
		       . " WHERE " . $this->where;
		$this->_statement = $this->query($query);
	}
	
	public function rewind ()
	{
		$this->refresh();
		$this->_statement->rewind();
	}

	public function valid ()
	{
		return $this->_statement->valid();
	}
	
	/**
	 * Establish a connection.
	 *
	 * Establishes a connection to a database defined by Model::$registry
	 * and tries to get the data by the where-clause.
	 *
	 * @since 0.1.0
	 *
	 * @param string $where SQL-Statement's condition
	 *
	 * @return Nothing except an instance of Model when used with
	   "new Model();"
	 *
	 * @section Changelog
	 * <table>
	 * <tr>
	 * 	<th>Version</th>
	 * 	<th>Description</th>
	 * </tr>
	 * </table>
	 *
	 * @section Examples
	 *
	 * @subsection Generate_Model New instance
	 *
	 * After you generated a new inherit of the model ("class Model_TableName
	 * extends Model {}") you're able to get the whole table or a subset of
	 * it:
	 * @code
	 * // get the whole table / model
	 * $mdl = new Model_TableName();
	 *
	 * // get only a subset according to conditions
	 * $subset = new Model_TableName("x > 42");
	 *
	 * // or get only one row by a precise condition
	 * $user = new Model_TableName("iduser = 123");
	 * @endcode
	 *
	 *
	 * @subsection Model_Injection Prevent SQL-Injection
	 *
	 * Some clients try to harm your site by inserting SQL in forms. You can
	 * prevent this by let rpd escape those statements:
	 * @code
	 * $critical_input = "' || '' == '";
	 * $user = new Model_TableName("name = '%s'", $critical_input);
	 *
	 * // and even more arguments:
	 * $userlist = new Model_TableName("age > %s && gender == '%s'",
	 *                                  $some_age,
	 *                                  $a_gender);
	 * @endcode
	 */
	function __construct($where = "1")
	{
		if ($this->table === NULL)
		{
			$this->table = RPD::ModelToTablename(get_called_class());
		}
		else
		{
			$this->table = $table;
		}

		$this->_joinedTables["this"] = $this;

		if ($where != "1")
		{
			if ($this->where == "1")
			{
				$this->where = $where;
			}
			else
			{
				$this->where .= " && (" . $where . ")";
			}
		}

		parent::__construct(Registry::get($this->registry));
	}
}

?>